Skip to content

Keep this host's paths and keys out of room lines - #2700

Merged
Birfy merged 1 commit into
mainfrom
fix/2699-room-line-leak
Nov 8, 2026
Merged

Birfy merged 1 commit into
mainfrom
fix/2699-room-line-leak

Conversation

@Birfy

@Birfy Birfy commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev

Fixes #2699
Owner: Dev
Reviewer: Reviewer

What changed

A room line is fanned out to every other home in the room (room-membership.js fanoutRoomLine), so it may not carry this machine's private details.

  • An unread picture in a group names the attachment, never a path. imagesForGroup used to hand attachmentFilePath(...) to imagePlaceholder, so the far side read The file is at "/tmp/…/attachments/v1/objects/ab/…"; your own file tools can open it. The group form (group: true) now drops the path even when one is passed in: it identifies the picture by file name (or sha256: prefix), media type and size, and says it could not be read. A Bot's own chat keeps the path, because that reader is this home's model and its own file tools can open it (Chat images cannot be read: the host has no vision model, and the placeholder hides where the bytes are #779).
  • A crashed external Bot reports only how it ended. The line reads crashed (exit 1) or crashed (signal SIGKILL); the child's last ~200 characters of stderr go to rt.warn. Before, crashed (exit 1: Error: ENOENT: … open '/data/dsh/bot/home/state.json' token=ghp_…) handed the room this host's folders and whatever the child had printed. A spawn or stdin failure reports the crash without the reason too.
  • One redaction entry for every room line. The store gate both writers already pass (room-context.js prepareRoomLine, from groups.js appendRoom and room-membership.js append) now also runs this host's rt.redactSecrets before a line is kept or sent. It is wired once at install (setRoomLineRedactor), so no line builder has to remember.
  • #1164/#2634 semantics are unchanged: exit/close still race with the first one winning, entry.reported still reports once, and a user stop still writes no crashed line.

Verification at the tip

Tip SHA: ba4f8ce

Ran:

  • cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/room-line-leak.test.js test/images.test.js test/room-external.test.js test/session-identity.test.js test/room-context.test.js test/room-membership.test.js test/remote-room.test.js test/group-work.test.js → 224 pass, 0 fail
  • cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/private-files.test.js test/store.test.js test/room-context.test.js test/room-line-leak.test.js → 54 pass, 0 fail
  • cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/{agents,archive-bot,authz,automation-default}.test.js one at a time → 16/6/21/14 pass (they aborted under full-suite concurrency, not from this change)
  • node scripts/ds-bot-patches.mjs check → ok 10 patches apply cleanly in order / ok core/ equals ds-bot/ + patches/ds-bot/
  • node core/scripts/build-client.mjs --check → exit 0
  • cd core && node dev/harness/node_modules/oxlint/bin/oxlint -c .oxlintrc.json index.js web-search.js src dev scripts test → 0 errors, 87 warnings (all pre-existing)
  • node .github/scripts/changelog.mjs check → 35 fragments well formed; node .github/scripts/check-mojibake.mjs --check → 0 occurrences; node .github/scripts/testing-layers.mjs --check → every test file classified

Not run (and why):

  • npm run verify (the whole suite) — the brief for this task says not to run it, and this machine has no git
  • node scripts/ds-bot-patches.mjs canonical and core/scripts/check-arch-docs.mjs — both need git, which this machine does not have; patches/ds-bot/ was rebuilt by hand (only added lines inside existing hunks, counts recounted) and proven with check's git-free applier, and docs/subsystems/core-host.md (the page that owns core/src/host/) is updated in this diff
  • the smoke test — needs a DSH checkout and a bench; nothing here touches index.js or the browser side

Red first: core/test/room-line-leak.test.js (4 cases, full-stack fixture with a remote seat and parseRoomShare over the wire bytes) failed 4/4 against the base implementation — the group line carried /tmp/…/attachments/v1/objects/ab/… and the crash line carried open '/data/dsh/bot/home/state.json' token=ghp_FAKE….

Checkpoint

{
  "pr": 2700,
  "claim": "Dev",
  "issue": 2699,
  "summary": "A room line is sent to every other home in the room, so it no longer carries this machine's private details. An unread picture in a group is named by file name, media type and size instead of an absolute path, while a Bot's own chat still names the path. A crashed external Bot's line reports only exit <code> or signal <name>, and the child's stderr tail stays in this host's log. Every stored room line also passes this host's secret redaction before it is kept or sent.",
  "files": ["core/src/host/images.js", "core/src/host/room-participant-external.js", "core/src/host/room-context.js", "core/src/host/groups.js", "core/src/host/room-membership.js", "core/test/room-line-leak.test.js", "core/test/images.test.js", "core/test/session-identity.test.js", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/0012-room-membership.patch", "docs/subsystems/core-host.md", "changelog.d/2699-room-line-leak.fixed.md"],
  "checks": { "ran": "node scripts/ds-bot-patches.mjs check; 12 core test files; oxlint; build-client --check", "result": "green", "at": "ba4f8ce1829ee4da7e30e6e62507e3106745e2bb" },
  "next": "none — ready for review"
}

A room line is not local: fanoutRoomLine sends it to every other home in the
room, so an unread picture must not name an absolute path of this machine and
a crashed external Bot's line must not carry the child's stderr tail. The
group placeholder now names the attachment by file name, media type and size
only, the crash line reports just exit <code> or signal <name> and keeps the
tail in rt.warn, and the store gate every writer already passes through runs
this host's secret redaction before a line is kept or sent.

Fixes #2699
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 8, 2026
@Birfy Birfy self-assigned this Nov 8, 2026
@Birfy

Birfy commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at ba4f8ce
Written-by: Reviewer
Attestation: ba4f8ce role=Reviewer

Ran at this tip:

$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/room-line-leak.test.js test/images.test.js test/room-external.test.js test/room-context.test.js test/session-identity.test.js test/room-membership.test.js test/groups.test.js
$ for f in src/host/room-context.js src/host/room-membership.js src/host/room-participant-external.js test/room-context.test.js test/room-external.test.js test/room-line-leak.test.js; do cmp -s "core/$f" "patches/ds-bot/files/$f" || echo "DIFF $f"; done
$ node scripts/ds-bot-patches.mjs check
$ for f in images.js room-participant-external.js room-context.js groups.js room-membership.js; do cp base/$f core/src/host/$f; done && cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/room-line-leak.test.js
# tests 89  # pass 89  # fail 0        (tip)
# tests 4   # pass 0   # fail 4        (the five changed src files at their base blobs, new tests kept)

What I checked.

  1. Both leaks are closed, and the assertion is on what the far side reads. room-line-leak.test.js:139-147 (assertNoLocalDetail) refuses a /ghp_[A-Za-z0-9]+/ and a /token=/i shape and this home's folder, and it is applied to the decoded room-share envelope, not to the local log: image placeholder :169-180, crash line :209-218, spawn-failure line :247-251, redaction :267-272. The group placeholder drops the path even when a caller passes one (images.js:75-79, :212); a crash line says crashed (exit 1) / (signal …) (from the ending), and the stderr tail only reaches rt.warn (room-participant-external.js:84-99, :204-214).
  2. The rewrite point is the single one before fan-out. Both writers of room.log pass prepareRoomLine: groups.js:89 (appendRoom, redactor wired once at install :239-241) and room-membership.js:413-421 (append → prepareRoomLine(line, { redact: rt.redactSecrets })). Every outbound op: 'line' envelope is inside fanoutRoomLine (:2207, :2242, :2261), and each of its callers (:884, :2082, groups.js:90) hands it a line that already went through the store gate — I found no second fan-out path (group envelope, <untrusted-message> wrapping and the projected-room packet all read the stored, redacted text).
  3. Nothing local was erased: the Bot's own chat still gets the path (images.js:212 group only; session-identity.test.js:230-240 asserts the chat placeholder keeps it), and the owner's log keeps the stderr tail and the spawn path (room-participant-external.js:88, :193, test :222-223, :251).
  4. External room participant: stop escalation, stderr visibility, timeout wording (#198 follow-ups) #1164/bug(host): a timed-out external room turn leaves its child running, so the next turn receives the stale answer #2634 semantics hold: one reported line, exit/close race unchanged, a stop writes no crashed line, DRAIN_MS untouched. The -9 lines in room-external.test.js are tightened, not loosened: crashed (exit 3) plus doesNotMatch(/missing config/) plus a warnings assertion that the tail stayed local (:237-253), and the recorded 200-character cap is now checked on the log rather than the room line.
  5. Generated artifacts agree: the six patches/ds-bot/files/ mirrors are byte-identical to core/ (cmp), ds-bot-patches.mjs check reports ok 10 patches apply cleanly in order and ok core/ equals ds-bot/ + patches/ds-bot/. The diff does not touch core/src/client, so client.js does not move (not in the 20-file compare).
  6. Red first: with images.js, room-participant-external.js, room-context.js, groups.js and room-membership.js at base 1913ebc7 and the new tests kept, all 4 room-line-leak cases fail.
  7. Regression: the affected set (leak, images, room-external, room-context, session-identity, room-membership, groups) is 89/89 at the tip; the Make one visible name one identity #2694/Fold canonical equivalents into the three identity keys that bypassed the name rule #2696 identity-key cases in session-identity.test.js are unchanged in behaviour and green.
  8. Scope clean: ds-bot/, plugins/** and the room-membership.js fan-out rules themselves are untouched (:2163 body unchanged); only the redaction wiring and the two leak sites moved. docs/subsystems/core-host.md and the changelog fragment are updated.

Not run here: build-client.mjs --check and oxlint — this box has no core/node_modules (esbuild/oxlint absent). CI package (client check) and tests-ok are green at this tip, so the generator half is covered there.

Checkpoint block is compliant (1287 chars, summary 472, checks.at = this tip); agent-pr present, no hold, Fixes #2699, Owner: Dev / Reviewer: Reviewer.

@github-actions github-actions Bot added the pass label Nov 8, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit 1783ddc Nov 8, 2026
17 of 19 checks passed
@Birfy
Birfy deleted the fix/2699-room-line-leak branch November 8, 2026 17:49
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A room line tells the far side this host's attachment path and a crashed child's stderr tail

1 participant